ποΈGitΠ―ΡΠ°ποΈ
.skills/testing-ci/SKILL.md 5235b8fc51eb5f46d453effb2deec00bcc6abdbb (5235b8fc) Text, 10.68 KB
Skill: Testing and CI Verification
Description
Guidelines and commands for verifying code changes locally and understanding the Meshtastic-Android CI pipeline. Use this to determine which testing matrix is needed based on the change type.
1) Baseline local verification order
Run in a single invocation for routine changes to ensure code formatting, analysis, and basic compilation:
T282828
./gradlew spotlessApply spotlessCheck detekt assembleDebug Tffa657test allTests
β Why no T383838clean? Incremental builds are safe and significantly faster. Only use T383838clean when debugging
β stale cache issues.
β Why T383838test allTests and not just T383838test: In KMP modules, the T383838test task name is ambiguous. Gradle
β matches both T383838testAndroid and T383838testAndroidHostTest and refuses to run either, silently skipping KMP
β modules. T383838allTests is the T383838KotlinTestReport lifecycle task registered by the KMP plugin.
β Conversely, T383838allTests does not cover pure-Android modules (T383838:androidApp, T383838:core:barcode, etc.),
β which is why both T383838test and T383838allTests are needed.
Note: If testing Compose UI on the JVM (Robolectric), pin tests to T383838@Config(sdk = [34]) to avoid SDK 35 compatibility crashes.
SharedFlow + backgroundScope in T383838runTest
When testing long-lived coroutines (e.g., T383838Flow.collect loops launched in T383838backgroundScope), use T383838runTest(UnconfinedTestDispatcher()) instead of plain T383838runTest:
T282828
T8b949e// β BAD β SharedFlow emissions silently never reach collectors
Tf0883e@Test Tff7b72fun Td2a8ff`inbound packet is forwarded`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3backgroundScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3sutTb4b4b4.Te6edf3startTb4b4b4(Te6edf3backgroundScopeTb4b4b4) Tb4b4b4}
Te6edf3sharedFlowTb4b4b4.Te6edf3emitTb4b4b4(Te6edf3packetTb4b4b4)
T8b949e// assertion fails β collector never receives the emission
Tb4b4b4}
T8b949e// β
GOOD β UnconfinedTestDispatcher eagerly dispatches subscriber resumptions
Tf0883e@Test Tff7b72fun Td2a8ff`inbound packet is forwarded`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3UnconfinedTestDispatcherTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3backgroundScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3sutTb4b4b4.Te6edf3startTb4b4b4(Te6edf3backgroundScopeTb4b4b4) Tb4b4b4}
Te6edf3sharedFlowTb4b4b4.Te6edf3emitTb4b4b4(Te6edf3packetTb4b4b4)
T8b949e// assertion passes β collector receives emission immediately
Tb4b4b4}
Why: T383838backgroundScope uses T383838StandardTestDispatcher by default, which does not eagerly dispatch T383838SharedFlow subscriber resumptions. Even T383838advanceUntilIdle() won't trigger delivery. T383838UnconfinedTestDispatcher() fixes this by dispatching eagerly. This affects any test where a coroutine in T383838backgroundScope collects from a T383838SharedFlow or T383838MutableSharedFlow.
2) Change-type verification matrix
β’ T383838docs-only changes: Usually no Gradle run required, but run T383838spotlessCheck if practical.
β’ T383838UI text/resource changes: T383838spotlessCheck, T383838detekt, T383838assembleDebug.
β’ T383838feature/commonMain logic changes: T383838spotlessCheck, T383838detekt, T383838test allTests, T383838assembleDebug.
β’ T383838navigation/DI wiring changes: T383838spotlessCheck, T383838detekt, T383838assembleDebug, T383838test allTests, plus flavor unit tests if available.
β’ If touching any KMP module, also run T383838kmpSmokeCompile.
β’ T383838worker/service/background changes: Broad tests, targeted WorkManager checks.
β’ T383838BLE/networking/core repository: T383838spotlessCheck, T383838detekt, T383838assembleDebug, T383838test allTests.
3) Flavor checks
Run these when relevant to map, provider, or flavor-specific behavior:
T282828
./gradlew lintFdroidDebug lintGoogleDebug
./gradlew testFdroidDebug testGoogleDebug
3b) Screenshot testing (two modules)
Compose Preview Screenshot Testing (AGP/layoutlib) is split into two modules β keep the distinction:
β’ T383838:screenshot-tests β visual-regression gate. CI runs T383838:screenshot-tests:validateDebugScreenshotTest. Holds atomic, dual-purpose components. Touching one of these previews is expected to move a gated baseline.
β’ T383838:docs-screenshots β generate-only, NOT validated in CI. Holds doc-framed compositions (crops/full screens tuned for the docs site). Reframe these freely; it never churns the regression gate.
T282828
./gradlew :screenshot-tests:updateDebugScreenshotTest T8b949e# regression goldens
./gradlew :docs-screenshots:updateDebugScreenshotTest T8b949e# doc-framed composition images
./gradlew :screenshot-tests:copyDocsScreenshots T8b949e# copy doc images from BOTH modules β docs/assets
Rendering is host-deterministic (layoutlib): a local T383838update produces references byte-identical to CI, so locally-recorded goldens pass T383838validate. T383838copyDocsScreenshots overwrites a stale committed T383838nodes_detail_local.png each run β T383838git checkout it. Public previews consumed cross-module by a wrapper need a T383838detekt-baseline.xml entry (PreviewPublic). New screenshot? Pick the module by purpose; see T383838docs/assets/screenshots/README.md.
3c) Fresh-install manual/agent testing: skip onboarding
Debug builds accept an intent extra to skip the intro flow (T383838MainActivity.kt, T383838BuildConfig.DEBUG-gated β never reaches release/Play builds). Pair with T383838pm grant (native Android, no app code) to pre-accept runtime permissions:
T282828
adb shell pm grant <pkg> android.permission.BLUETOOTH_SCAN
adb shell pm grant <pkg> android.permission.BLUETOOTH_CONNECT
adb shell pm grant <pkg> android.permission.ACCESS_FINE_LOCATION
adb shell pm grant <pkg> android.permission.POST_NOTIFICATIONS T8b949e# API 33+
adb shell am start -n <pkg>/org.meshtastic.app.MainActivity --ez skip_onboarding Tffa657true
Use this whenever driving the app from a fresh install/uninstall (screenshot tests, UI automation, agent-driven exploration) instead of clicking through the intro screens.
4) CI Pipeline Architecture
CI is defined in T383838.github/workflows/reusable-check.yml and structured as parallel job groups:
1. T383838lint-check β Runs spotless, detekt, Android lint, and KMP smoke compile in a single Gradle invocation (avoids 3x cold-start overhead). Uses T383838fetch-depth: 0 (full clone) for spotless ratcheting and version code calculation. Produces T383838cache_read_only output and computed T383838version_code for downstream jobs.
2. T383838test-shards β A 3-shard matrix that runs unit tests in parallel (depends on T383838lint-check):
β’ T383838shard-core: T383838allTests for all T383838core:* KMP modules.
β’ T383838shard-feature: T383838allTests for all T383838feature:* KMP modules.
β’ T383838shard-app: Explicit test tasks for pure-Android/JVM modules (T383838androidApp, T383838desktopApp, T383838core:barcode).
Each shard generates Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags.
Downstream jobs use T383838fetch-depth: 1 and receive T383838VERSION_CODE from lint-check via env var, enabling shallow clones.
3. T383838android-check β Builds APKs for all flavors (depends on T383838lint-check).
4. T383838build-desktop β Multi-OS matrix (T383838macos-latest, T383838windows-latest, T383838ubuntu-24.04, T383838ubuntu-24.04-arm) that builds desktop distributions via T383838createDistributable (depends on T383838lint-check).
5. T383838screenshot-check β Runs T383838:screenshot-tests:validateDebugScreenshotTest (the visual-regression gate) and uploads a diff report. Note: T383838:docs-screenshots is intentionally NOT validated here (generate-only).
Runner Strategy (Three Tiers)
β’ T383838ubuntu-24.04-arm β Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). Benefits from ARM runners' shorter queue times.
β’ T383838ubuntu-24.04 β Main Gradle-heavy jobs (CI T383838lint-check/T383838test-shards/T383838android-check, release builds, Dokka, publish, dependency-submission). Pin for reproducibility.
β’ Desktop runners: Multi-OS matrix (T383838macos-latest, T383838windows-latest, T383838ubuntu-24.04, T383838ubuntu-24.04-arm) for the T383838build-desktop job and release packaging.
CI Gradle Properties
T383838gradle.properties is tuned for local dev (8g heap, 4g Kotlin daemon). CI uses T383838.github/ci-gradle.properties, which the T383838gradle-setup composite action copies to T383838~/.gradle/gradle.properties. Key CI overrides:
β’ T383838org.gradle.daemon=false (single-use runners)
β’ T383838kotlin.incremental=false (fresh checkouts)
β’ T383838-Xmx4g Gradle heap, T383838-Xmx2g Kotlin daemon
β’ VFS watching disabled, workers capped at 4
β’ T383838org.gradle.isolated-projects=true for better parallelism
β’ Disables unused Android build features (T383838resvalues, T383838shaders)
CI Conventions
β’ KMP Smoke Compile: T383838./gradlew kmpSmokeCompile is a lifecycle task (registered in T383838RootConventionPlugin) that auto-discovers all KMP modules and depends on their T383838compileKotlinJvm + T383838compileKotlinIosSimulatorArm64 tasks.
β’ T383838maxParallelForks CI logic: T383838ProjectExtensions.kt checks T383838project.findProperty("ci") == "true" and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass T383838-Pci=true.
β’ Detekt report formats: Detekt.kt checks T383838project.findProperty("ci") == "true" and disables html, txt, md reports in CI; only xml + sarif are retained for GitHub annotations.
β’ Robolectric SDK caching: The T383838gradle-setup composite action caches T383838~/.m2/repository/org/robolectric to prevent flaky T383838SocketException on SDK downloads. Cache key is T383838robolectric-{version}-sdk{level} β update when bumping version or SDK level.
β’ T383838mavenLocal() gated: Disabled by default to prevent CI cache poisoning. Pass T383838-PuseMavenLocal for local JitPack testing.
β’ JUnit parallel execution: Enabled project-wide with classes running sequentially (T383838junit.jupiter.execution.parallel.mode.classes.default=same_thread) to avoid T383838Dispatchers.setMain() races. Cross-module parallelism comes from Gradle forks (T383838maxParallelForks).
β’ T383838test-retry plugin: Applied to all module types (maxRetries=2, maxFailures=10).
β’ T383838fail-fast: false: Test sharding does not cancel other shards on failure.
β’ Explicit Gradle task paths: Prefer T383838app:lintFdroidDebug over shorthand T383838lintDebug in CI.
β’ Pull request CI: Main-only (T383838.github/workflows/pull-request.yml targets T383838main).
β’ Merge queue hygiene: T383838merge-queue.yml cancels superseded runs for the same PR (GitHub does not auto-cancel destroyed merge-group runs) and skips the heavy pipeline for docs-only entries (T383838docs/**, T383838*.md). T383838rb-check runs ONLY in the merge queue. T383838main-check.yml passes T383838run_lint: false β every main commit is a merge-queue-verified merge commit, so main pushes only rebuild the debug APKs for the snapshot release.
β’ Cache writes: Trusted on T383838main and merge queue runs; other refs use read-only cache.
β’ Path filtering: T383838check-changes in T383838pull-request.yml must include module dirs plus build/workflow entrypoints (T383838build-logic/**, T383838gradle/**, T383838.github/workflows/**, T383838gradlew, T383838settings.gradle.kts, etc.).
β’ AboutLibraries: Runs in T383838offlineMode by default (no GitHub/SPDX API calls). Release builds pass T383838-PaboutLibraries.release=true via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on T383838CI or T383838GITHUB_TOKEN alone.
Served by rngit 1.5.2 - Generated in 0.05s